#import "RNSearchMenuView.h"
#import "RNConstants.h"
#import "NSShadow+MCAdditions.h"

CGFloat const RNSearchMenuCornerRadius = 5.0f;
CGFloat const RNSearchMenuShadowRadius = 10.0f;
CGFloat const RNSearchMenuSeparatorLineHeight = 1.0f;
NSUInteger const RNSearchMenuIntercellSpacing = 2;
NSUInteger const RNSearchMenuBottomPadding = 8;
NSUInteger const RNSearchMenuTopPadding = 5;
NSUInteger const RNSearchMenuRowLeftPadding = 20;
NSUInteger const RNSearchMenuRowRightPadding = 10;
NSUInteger const RNSearchMenuDefaultRowHeight = 20;
NSUInteger const RNSearchMenuSeparatorRowHeight = 10;
NSString * const RNSearchMenuItemYPositionKey = @"RNSearchMenuItemYPosition";
NSString * const RNSearchMenuIsSeparatorKey = @"isSeparator";

static NSGradient *searchMenuTableSelectionBackgroundGradient;
static NSColor *searchMenuBackgroundColor;
static NSColor *searchMenuSelectionBorderColor;
static NSColor *searchMenuSeparatorColor;
static NSShadow *searchMenuShadow;

@interface RNSearchMenuView (RNSearchMenuViewPrivateMethods)
-(void)resetBorderPath_;
-(void)selectRowAtPoint_:(NSPoint)point;
-(NSUInteger)findRowInRange_:(NSRange)rowRange containingPoint:(NSPoint)point;
-(NSRange)rowsInRect_:(NSRect)rect;
-(void)drawSeparatorInRect_:(NSRect)separatorRect;
-(NSRect)textRectWithAttributedString_:(NSAttributedString *)title fromRowRect:(NSRect)rowRect;
-(CGFloat)itemYPositionAtIndex_:(NSUInteger)itemIndex;
@end

@implementation RNSearchMenuView

@synthesize delegate;
@synthesize selectedRow = selectedRow_;
@synthesize menuItems = menuItems_;
@synthesize borderPath = borderPath_;
@synthesize selectedRowRect = selectedRowRect_;
@synthesize defaultTextAttributes = defaultTextAttributes_;
@synthesize selectedTextAttributes = selectedTextAttributes_;
@synthesize trackingMouseClick = trackingMouseClick_;
@synthesize action = action_;
@synthesize target = target_;


#pragma mark -
#pragma mark Initialization

+(void)initialize {
    searchMenuTableSelectionBackgroundGradient = [[NSGradient alloc] initWithStartingColor:[NSColor colorWithCalibratedRed:0.1f green:0.26f blue:0.95f alpha:1.0] endingColor:[NSColor colorWithDeviceRed:0.32f green:0.44f blue:0.97f alpha:1.0f]];
    searchMenuBackgroundColor = [NSColor colorWithCalibratedRed:0.507 green:0.532 blue:0.561 alpha:0.8];
    searchMenuSelectionBorderColor = [NSColor colorWithCalibratedRed:0.28f green:0.4f blue:0.92f alpha:1.0f];
    searchMenuSeparatorColor = [NSColor colorWithCalibratedWhite:0.8f alpha:1.0f];
    searchMenuShadow = [[NSShadow alloc] initWithColor:[NSColor colorWithCalibratedWhite:0.0f alpha:0.8f] offset:NSZeroSize blurRadius:RNSearchMenuShadowRadius];
}

-(id)initWithFrame:(NSRect)frame {
    if (!(self = [super initWithFrame:frame]))
        return nil;
    
    NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInActiveApp) owner:self userInfo:nil];
    [self addTrackingArea:trackingArea];
    [self setSelectedRow:-1];
    [self setMenuItems:[NSMutableArray array]];
    [self setDefaultTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[NSFont systemFontOfSize:[NSFont systemFontSize]], NSFontAttributeName, searchMenuSeparatorColor, NSForegroundColorAttributeName, nil]];
    [self setSelectedTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[NSFont systemFontOfSize:[NSFont systemFontSize]], NSFontAttributeName, [NSColor whiteColor], NSForegroundColorAttributeName, nil]]; 
    [self setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
    return self;
}


#pragma mark -
#pragma mark Event handling

-(void)mouseEntered:(NSEvent *)event {
    [self selectRowAtPoint_:[self convertPoint:[event locationInWindow] fromView:nil]];
}

-(void)mouseMoved:(NSEvent *)event {
    NSPoint mousePosition = [self convertPoint:[event locationInWindow] fromView:nil];
    if (![self mouse:mousePosition inRect:[self selectedRowRect]])
        [self selectRowAtPoint_:mousePosition];
}

-(void)mouseExited:(NSEvent *)event {
    [self setNeedsDisplayInRect:[self selectedRowRect]];
    [self setSelectedRow:-1];
    [self setTrackingMouseClick:NO];
}

-(void)mouseDown:(NSEvent *)event {
    [self setTrackingMouseClick:YES];
}

-(void)mouseUp:(NSEvent *)event {
    if (![self trackingMouseClick])
        return;
    [self setTrackingMouseClick:NO];
    [NSApp sendAction:[self action] to:[self target] from:self];
}

-(void)updateTrackingAreas {
    for (NSTrackingArea *trackingArea in [self trackingAreas])
        [self removeTrackingArea:trackingArea];
    
    NSTrackingArea *newTrackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInActiveApp) owner:self userInfo:nil];
    [self addTrackingArea:newTrackingArea];
}

-(BOOL)acceptsFirstResponder {
    return NO;
}


#pragma mark -
#pragma mark Drawing

-(void)drawRect:(NSRect)frame {
    [searchMenuBackgroundColor setFill];
    
    [NSGraphicsContext saveGraphicsState];
    [searchMenuShadow set];
    [[self borderPath] fill];
    [NSGraphicsContext restoreGraphicsState];
        
    [self drawSelectionHighlight];
    NSRange dirtyRects = [self rowsInRect_:frame];
    [self drawRowsInRange:dirtyRects];
}

-(void)drawSelectionHighlight {
    if ([self selectedRow] == -1)
        return;
    NSRect drawRect = NSInsetRect([self selectedRowRect], 1.0f, 1.0f);
    [searchMenuTableSelectionBackgroundGradient drawInRect:drawRect angle:-90];
    [searchMenuSelectionBorderColor setStroke];
    [NSBezierPath strokeRect:drawRect];
}

-(void)drawRowsInRange:(NSRange)dirtyRects {
    NSRect currentRect = [[self borderPath] bounds];
    for (NSUInteger itemIndex = dirtyRects.location; itemIndex < NSMaxRange(dirtyRects); itemIndex++) {
        currentRect.origin.y = [self itemYPositionAtIndex_:itemIndex];
        BOOL isSeparator = [self isSeparatorAtIndex:itemIndex];
        currentRect.size.height = (isSeparator ? RNSearchMenuSeparatorRowHeight : RNSearchMenuDefaultRowHeight);
        if (isSeparator) {
            [self drawSeparatorInRect_:currentRect];
            continue;
        }
        NSAttributedString *title = [[NSAttributedString alloc] initWithString:[self itemTitleAtIndex:itemIndex] attributes:((NSInteger)itemIndex == [self selectedRow] ? [self selectedTextAttributes] : [self defaultTextAttributes])];
        NSRect textRect = [self textRectWithAttributedString_:title fromRowRect:currentRect];
        [title drawWithRect:textRect options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine | NSLineBreakByCharWrapping];
    }
}

-(void)drawSeparatorInRect_:(NSRect)separatorRect {
    [searchMenuSeparatorColor setFill];
    NSRect line = NSMakeRect(NSMinX(separatorRect), floor(NSMidY(separatorRect)), NSWidth(separatorRect), RNSearchMenuSeparatorLineHeight);
    [NSBezierPath fillRect:line];
}


#pragma mark -
#pragma mark Layout

-(BOOL)isFlipped {
    return YES;
}

-(NSRect)rectOfRow:(NSUInteger)rowIndex {
    if (rowIndex >= [[self menuItems] count] || rowIndex < 0)
        return NSZeroRect;
    
    BOOL isSeparator = [self isSeparatorAtIndex:rowIndex];
    CGFloat minY = [self itemYPositionAtIndex_:rowIndex];
    NSRect borderRect = [[self borderPath] bounds];
    return NSMakeRect(NSMinX(borderRect), minY, NSWidth(borderRect), (isSeparator ? RNSearchMenuSeparatorRowHeight : RNSearchMenuDefaultRowHeight));
}

-(void)selectRowAtPoint_:(NSPoint)point {
    NSInteger row = [self findRowInRange_:NSMakeRange(0, [[self menuItems] count]) containingPoint:point];
    BOOL isSeparator = [self isSeparatorAtIndex:row];
    if (isSeparator)
        return;
    [self setSelectedRow:row];
}

-(NSUInteger)findRowInRange_:(NSRange)rowRange containingPoint:(NSPoint)point {
    NSUInteger middleRow = floor(rowRange.location + (rowRange.length / 2));
    CGFloat middleRowMinY = [self itemYPositionAtIndex_:middleRow];
    BOOL isSeparator = [self isSeparatorAtIndex:middleRow];
    CGFloat middleRowMaxY = middleRowMinY + (isSeparator ? RNSearchMenuSeparatorRowHeight : RNSearchMenuDefaultRowHeight);
    
    if (point.y < (middleRowMinY - (RNSearchMenuIntercellSpacing / 2))) 
        return (middleRow > 0 ? [self findRowInRange_:NSMakeRange(rowRange.location, middleRow - rowRange.location) containingPoint:point] : 0);
    if (point.y >= (middleRowMaxY + (RNSearchMenuIntercellSpacing / 2)))
        return (middleRow == ([[self menuItems] count] - 1) ? middleRow : [self findRowInRange_:NSMakeRange(middleRow, rowRange.location + rowRange.length - middleRow) containingPoint:point]);
    return middleRow;
}

-(NSRange)rowsInRect_:(NSRect)rect {
    NSInteger firstRow = [self findRowInRange_:NSMakeRange(0, [[self menuItems] count]) containingPoint:rect.origin];
    NSInteger lastRow = [self findRowInRange_:NSMakeRange(0, [[self menuItems] count]) containingPoint:NSMakePoint(NSMaxX(rect), NSMaxY(rect))];
    return NSMakeRange(firstRow, lastRow - firstRow + 1);
}

-(NSRect)textRectWithAttributedString_:(NSAttributedString *)title fromRowRect:(NSRect)rowRect {
    NSRect textRect = rowRect;
    textRect.origin.x += RNSearchMenuRowLeftPadding;
    textRect.size.width -= RNSearchMenuRowLeftPadding + RNSearchMenuRowRightPadding;
    textRect = NSInsetRect(textRect, 0.0f, (NSHeight(textRect) - [title size].height) / 2);
    return textRect;
}

-(void)setFrame:(NSRect)newFrame {
    [super setFrame:newFrame];
    [self resetBorderPath_];
}

-(CGFloat)preferredHeight {
    CGFloat preferredHeight = RNSearchMenuTopPadding + RNSearchMenuBottomPadding + RNSearchMenuShadowRadius;
    for (NSDictionary *item in [self menuItems]) {
        preferredHeight += ([[item valueForKey:RNSearchMenuIsSeparatorKey] boolValue] ? RNSearchMenuSeparatorRowHeight : RNSearchMenuDefaultRowHeight);
        preferredHeight += RNSearchMenuIntercellSpacing;
    }
    if ([[self menuItems] count] != 0)
        preferredHeight -= RNSearchMenuIntercellSpacing;
    return preferredHeight;
}


#pragma mark -
#pragma mark Data methods

-(void)update {
    NSUInteger count = [[self delegate] numberOfItemsInMenu];
    if (count == 0) {
        [[self menuItems] removeAllObjects];
        return;
    }
    NSMutableArray *items = [NSMutableArray arrayWithCapacity:count];
    NSUInteger itemIndex = 0;
    CGFloat currentY = RNSearchMenuTopPadding;
    while (itemIndex < count) {
        
        NSMutableDictionary *itemDictionary = [NSMutableDictionary dictionary];
        NSString *title = [[self delegate] itemTitleAtIndex:itemIndex];
        BOOL isSeparator = [title isEqualToString:RNSearchMenuSeparatorItem];

        [itemDictionary setValue:(isSeparator ? nil : title) forKey:RNTitleKey];
        [itemDictionary setValue:[NSNumber numberWithBool:isSeparator] forKey:RNSearchMenuIsSeparatorKey];
        [itemDictionary setValue:[NSNumber numberWithFloat:currentY] forKey:RNSearchMenuItemYPositionKey];
        [items addObject:itemDictionary];
        
        CGFloat itemHeight = isSeparator ? RNSearchMenuSeparatorRowHeight : RNSearchMenuDefaultRowHeight;
        currentY += itemHeight + RNSearchMenuIntercellSpacing;
        
        itemIndex++;
    }
    [self setMenuItems:items];

    while (![self isValidIndex:[self selectedRow]] || [self isSeparatorAtIndex:[self selectedRow]] || [self selectedRow] >= (NSInteger)(count - 1)) {
        if ([self selectedRow] == -1) {
            [self setSelectedRow:([self isValidIndex:0] && ![self isSeparatorAtIndex:0]) ? 0 : -1];
            return;
        }
        [self setSelectedRow:[self selectedRow] - 1];
    }
}

-(void)setSelectedRow:(NSInteger)row {
    NSRect oldSelectionRect = [self rectOfRow:selectedRow_];
    NSRect newSelectionRect = [self rectOfRow:row];
    selectedRow_ = row;
    [self setSelectedRowRect:newSelectionRect];
    NSRect dirtyRect = NSUnionRect(oldSelectionRect, newSelectionRect);
    [self setNeedsDisplayInRect:dirtyRect];
}

-(NSString *)itemTitleAtIndex:(NSUInteger)itemIndex {
    if (![self isValidIndex:itemIndex] || [self isSeparatorAtIndex:itemIndex])
        return nil;
    return [[[[self menuItems] objectAtIndex:itemIndex] valueForKey:RNTitleKey] copy];
}

-(BOOL)isSeparatorAtIndex:(NSUInteger)itemIndex {
    if (![self isValidIndex:itemIndex])
        return NO;
    return [[[[self menuItems] objectAtIndex:itemIndex] valueForKey:RNSearchMenuIsSeparatorKey] boolValue];
}

-(CGFloat)itemYPositionAtIndex_:(NSUInteger)itemIndex {
    if (![self isValidIndex:itemIndex])
        return -1;
    return [[[[self menuItems] objectAtIndex:itemIndex] valueForKey:RNSearchMenuItemYPositionKey] floatValue];
}

-(BOOL)isValidIndex:(NSUInteger)itemIndex {
    if (itemIndex < 0 || itemIndex >= [[self menuItems] count])
        return NO;
    return YES;
}


#pragma mark -
#pragma mark Internal

-(void)resetBorderPath_ {
    NSBezierPath *border = [NSBezierPath bezierPath];
    NSRect borderRect = NSInsetRect([self bounds], RNSearchMenuShadowRadius, RNSearchMenuShadowRadius);
    borderRect.origin.y -= RNSearchMenuShadowRadius;
    borderRect.size.height += RNSearchMenuShadowRadius;
    CGFloat maxX = NSMaxX(borderRect);
    CGFloat minX = NSMinX(borderRect);
    CGFloat minY = NSMinY(borderRect);
    CGFloat maxY = NSMaxY(borderRect);
    [border moveToPoint:borderRect.origin];
    [border lineToPoint:NSMakePoint(maxX, minY)];
    [border lineToPoint:NSMakePoint(maxX, maxY - RNSearchMenuCornerRadius)];
    [border appendBezierPathWithArcFromPoint:NSMakePoint(maxX, maxY) toPoint:NSMakePoint(maxX - RNSearchMenuCornerRadius, maxY) radius:RNSearchMenuCornerRadius];
    [border lineToPoint:NSMakePoint(minX + RNSearchMenuCornerRadius, maxY)];
    [border appendBezierPathWithArcFromPoint:NSMakePoint(minX, maxY) toPoint:NSMakePoint(minX, maxY - RNSearchMenuCornerRadius) radius:RNSearchMenuCornerRadius];
    [border closePath];
    [self setBorderPath:border];
    [self setSelectedRowRect:[self rectOfRow:[self selectedRow]]];
}

@end
